/**
 * 
 */
package network.internal.communication;

import static commons.Preconditions.checkIsPositive;
import static commons.Preconditions.checkNotNull;
import static network.util.NetworkUtil.checkSocketState;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.Arrays;

import network.communication.Connection;
import network.io.InputStreamFactory;
import network.io.OutputStreamFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import commons.exception.ClosedConnectionException;

/**
 * The default implementation of Connection. It uses streams generated by stream
 * factories to handle data.
 * 
 * @author Armstrong Mardilson da Silva Goes
 */
public class DefaultConnection implements Connection {

	private static final Logger			logger	= LoggerFactory
														.getLogger(DefaultConnection.class);
	private final ObjectInputStream		input;
	private final ObjectOutputStream	output;
	private final Socket				socket;
	private final int					transferMaxBufferSize;

	/**
	 * Constructs a new DefaultConnection instance. This constructor calls the
	 * given stream factories to create the streams used to read and write data
	 * using the given Socket.
	 * 
	 * @param inputStreamFactory
	 *            The factory used to create the stream to read.
	 * @param outputStreamFactory
	 *            The factory used to create the stream to write.
	 * @param socket
	 *            The socket used to create the streams and to provide other
	 *            informations to the Connection.
	 * @param transferMaxBufferSize
	 *            This value is used while writing a ByteBuffer. It represents
	 *            the maximum amount of bytes written in the Connection's output
	 *            stream at a time.
	 * @throws IOException
	 *             If socket is closed, or if any of the stream factories throw
	 *             IOException while creating the streams or a operation on a
	 *             stream throws an IOException.
	 * @throws IllegalArgumentException
	 *             If any of the arguments is null or transferMaxBufferSize is
	 *             not positive.
	 */
	public DefaultConnection(	final InputStreamFactory<Socket, ObjectInputStream> inputStreamFactory,
								final OutputStreamFactory<Socket, ObjectOutputStream> outputStreamFactory,
								final Socket socket,
								final int transferMaxBufferSize)
			throws IOException {

		checkNotNull("inputStreamFactory cannot be null.", inputStreamFactory);
		checkNotNull("outputStreamFactory cannot be null.", outputStreamFactory);
		checkNotNull("socket cannot be null.", socket);
		checkSocketState(socket);
		checkIsPositive("transferMaxBufferSize must be positive.",
				transferMaxBufferSize);

		this.socket = socket;
		this.transferMaxBufferSize = transferMaxBufferSize;

		output = outputStreamFactory.createOutputStream(socket);
		output.flush();

		input = inputStreamFactory.createInputStream(socket);
	}

	private void checkOpenedConnection() throws ClosedConnectionException {
		if (isClosed()) {
			throw new ClosedConnectionException(this);
		}
	}

	@Override
	public void close() throws IOException, ClosedConnectionException {
		checkOpenedConnection();
		output.flush();
		output.close();
		socket.close();
	}

	@Override
	public void finalize() throws Throwable {
		if (!isClosed()) {
			close();
		}
		super.finalize();
	}

	@Override
	public boolean isClosed() {
		return socket.isClosed();
	}

	@Override
	public Byte readByte() throws IOException, ClosedConnectionException {
		checkOpenedConnection();
		Byte read = null;
		try {
			read = (Byte) input.readObject();
		} catch (final ClassNotFoundException e) {
			throw new IOException(e);
		}
		return read;
	}

	@Override
	public ByteBuffer readByteBuffer() throws IOException,
			ClosedConnectionException {
		checkOpenedConnection();
		byte[] data = null;
		try {
			data = (byte[]) input.readObject();
		} catch (final ClassNotFoundException e) {
			throw new IOException(e);
		}
		return ByteBuffer.wrap(data);
	}

	@Override
	public String readString() throws IOException, ClosedConnectionException {
		checkOpenedConnection();
		String read = null;
		try {
			read = (String) input.readObject();
		} catch (final ClassNotFoundException e) {
			throw new IOException(e);
		}
		return read;
	}

	private void writeArray(final byte[] data, final int start, final int end)
			throws IOException {
		if (start == end) {
			output.writeObject(new byte[0]);
		} else {
			for (int i = start; i < end; i += transferMaxBufferSize) {
				final int endPosition = Math
						.min(i + transferMaxBufferSize, end);
				output.writeObject(Arrays.copyOfRange(data, i, endPosition));
			}
		}
	}

	@Override
	public void writeByte(final Byte toWrite) throws IOException,
			ClosedConnectionException {
		checkOpenedConnection();
		logger.debug("writing {}", toWrite);
		output.writeObject(toWrite);
	}

	@Override
	public void writeByteBuffer(final ByteBuffer buffer) throws IOException,
			ClosedConnectionException {
		checkNotNull("buffer must not be null.", buffer);
		checkOpenedConnection();
		logger.debug("writing {}", buffer);
		writeArray(buffer.array(), buffer.position(), buffer.limit());
	}

	@Override
	public void writeString(final String toWrite) throws IOException,
			ClosedConnectionException {
		checkNotNull("toWrite must not be null.", toWrite);
		checkOpenedConnection();
		output.writeObject(toWrite);
	}
}